Exception handling 是大多數程式語言都有的語法,通常是用來避免各種錯誤狀況直接讓程式 shut dowm,還有讓這些錯誤發生時可以改用其他方式處理。與 if/else 的意義不同,有時候我們如果沒有去處理這些異常狀況可能連判斷 statement 都來不及就導致錯誤發生。
在 Solidity 中有幾種 error 的處理方法,分別是遠古時期的 throe
,0.4.10 版的 Solidity 新增的 require()
, assert()
, revert()
這三個語法以及 0.6 之後新增的 try/catch
。
require()
用來檢查較不嚴重的錯誤,通常是在執行前就檢驗合理的輸入或條件,可以退回使用到的 gasassert()
用來檢查較嚴重的錯誤,會像以前一樣拿走所有的手續費revert()
跟 require()
基本上相同,但是 revert()
沒有包括狀態檢查,而是在經過判斷之後直接回傳 error msgtry/catch
可以使用於外部執行函數和合約建立時(在一個合約裡面建立另外一個合約時)的錯誤,可以利用類條件式選擇來排除錯誤情況。等下會更詳細的敘述這些異常處理的語法。
Require 常常用於檢驗簡單的條件,當 require 被觸發時可以回復交易狀況。當一個 get function(read-only) 函式或者沒有宣告 payable
的 function 如果接收到了 ether 也會觸發 require
錯誤,這筆交易會失敗而且退回到沒有接收匯款前的狀態。總而言之交易失敗時或者各種輸入錯誤(檢查 Input 合法與否)時的錯誤會是以「回復到交易前的狀態」的 require
為主。
require(input > min, "Input must be greater than min!");
Assert 常常被用來檢測內部錯誤,出現錯誤時不會退回到交易前的狀態,會消耗全部的 gas,使程式運作強制中止。例如常數不變量、記憶體溢位或非法訪問資料結構、位元移動運算距離為負等等,都屬於 assert
的範疇。
asser(input == 0);
Revert 跟 Require 依樣會回復交易狀態,只是沒有檢測 statement 的過程,而是遇到 revert
時就會直接出現錯誤,所以可以用於判斷複雜狀態,例如把 revert
包在巢狀 if-else 中過濾各種情形,最後如果遇到了 revert
就回傳 error msg。
pragma solidity ^0.8.11;
contract error {
function testRevert(uint _i) public pure {
if (_i <= 10) {
if (_i != 10){
revert("Input must be equal 10");
}
}
}
}
使用異常處理的最主要目的是使當前的執行被停止或撤銷,並且把原先改變的狀態回復到原本狀態,包含帳戶在原先行為後的餘額。在異常發生時,Solidity 會執行一個回退的操作(0xfd)並且讓 EVM 把所有「狀態改變」回復到原先的狀態。
在進到更深入的 revert/require error msg 以及 try/catch
之前,我們先看一下 solidity 中的 error msg 格式與 error type。
在 Solidity 中 error msg 回傳的格式為:
回傳的 error type 有兩種,分別是 Panic 和 Error,定義方式為:
keccak256("Panic(uint256)")
:用於嚴重錯誤(內部記憶體錯誤)keccak256("Error(string)"
:用於正常錯誤require 回傳的錯誤類型為 error,而 assert 回傳的錯誤類型為 panic。
我們可以透過查表知道 panics 的錯誤訊息是什麼:
assert
with an argument that evaluates to false.unchecked { ... }
block.5 / 0
or 23 % 0
).x[i]
where i >= x.length
or i < 0
).我們也可以自訂 Error Terms,然後使用 revert 回傳這個 error type。
error MyError(uint256 _errorCode, uint256 _balance);
function throwMyError(bool _fail) external {
if (_fail) {
revert MyError(23, bal);
}
val = 29;
}
綜上所述,其實在客製完 error type 以及妥善在 function 布置好異常處理的情形下,我們是能透過「錯誤」來回傳訊息的,例如在 nestes call 中我們能夠透過不斷 revert 一個定義好的 error type 來做到在不同 contract 傳遞模組化訊息的目的!
這是非常高階的技巧,詳細內容可以看我之前發表在 TEM 的文章:以 EIP-3668 進行鏈下 / 跨鏈資料傳遞。
終於回到 try/catch
了,我們可以直接看官方文件的例子:
function doStuff4(bool _fail) external {
try conB.stuff1(_fail) returns (uint256 v) {
val = v;
return;
} catch Error(string memory reason) {
...
// revert(reason);
} catch Panic(uint256 errorCode) {
...
} catch (bytes memory lowLevelData) {
...
}
}
在其中我們可以看到三種 error types,包含:
catch Error(string memory reason) { ... }
:這跟我們之前提到的 error msg type 中的 "Error" 是一樣的,可以直接包裝 string
並進行異常處理。catch Panic(uint256 errorCode) { ... }
:與我們之前提到的 revert 的 "Panic" error msg tpye 相同,需要去判斷 errorCode
為何知道 assert
原因是什麼。catch (bytes memory lowLevelData) { ... }
:如果並非以上兩者也有 low-level error data 的選項。catch { ... }
來做異常處理。需要特別注意如果是使用 low-level functions 例如:call
、delegatecall
、staticcall
。錯誤時會回傳 false
而不是產生 error,所以我們要去 catch 這個 false
。
例如:
function claim() public payable {
require(!claimed[msg.sender], "You have been claimed!");
claimed[msg.sender] = true;
uint256 amountInEther = 0.001 ether;
(bool sent, bytes memory data) = payable(msg.sender).call{
value: amountInEther
}("");
require(sent, "Failed to withdraw Ether");
}
最後歡迎大家拍打餵食大學生
0x2b83c71A59b926137D3E1f37EF20394d0495d72d